/**
 * This file is released under the GNU General Public License.
 * Refer to the COPYING file distributed with this package.
 *
 * Copyright (c) 2008-2009 WURFL-Pro srl
 */
package net.sourceforge.wurfl.core.handlers.matchers;

import java.util.SortedSet;

import net.sourceforge.wurfl.core.Constants;
import net.sourceforge.wurfl.core.handlers.Handler;
import net.sourceforge.wurfl.core.handlers.classifiers.FilteredDevices;
import net.sourceforge.wurfl.core.request.WURFLRequest;
import net.sourceforge.wurfl.core.utils.StringUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Abstract implementation of {@link Matcher}.
 * 
 * <p>
 * It provides basic functionalities of matching. The matching strategy is:
 * <ul>
 * <li>Direct matching</li>
 * <li>Conclusive matching</li>
 * <li>Recovery matching</li>
 * <li>CatchAll matching</li>
 * </ul>
 * </p>
 * 
 * @author WURFL-PRO SRL, Rome, Italy
 * @version $Id: AbstractMatcher.java 1117 2009-03-20 10:07:30Z filippo.deluca $
 */
public abstract class AbstractMatcher implements Matcher {

	/** The handler used to determine if this matcher can handle the request */
	private final Handler handler;

	/** Log */
	protected final Log logger = LogFactory.getLog(getClass());

	/** Log undetected */
	protected final Log undetected = LogFactory
			.getLog(Constants.UNDETECTED_WURFL_DEVICES);

	/** Log detected */
	protected final Log detected = LogFactory
			.getLog(Constants.DETECTED_WURFL_DEVICES);

	/**
	 * Build AbstractMatcher by handler and collected devices by user-agent.
	 * 
	 * @param handler
	 *            The handler used to decide if this class can handle the
	 *            request.
	 */
	public AbstractMatcher(Handler handler) {
		this.handler = handler;
	}

	public boolean canHandle(WURFLRequest request) {
		return this.handler.canHandle(request.getUserAgent());
	}

	/**
	 * Applies the matching strategy. this strategy is valid for every subclass.
	 * <p>
	 * The strategy is:
	 * <ul>
	 * <li>Direct matching</li>
	 * <li>Conclusive matching</li>
	 * <li>Recovery matching</li>
	 * <li>CatchAll matching</li>
	 * </ul>
	 * </p>
	 */
	public final String match(WURFLRequest request,
			FilteredDevices filteredDevices) {

		String userAgent = this.handler.normalize(request.getUserAgent());

		// 1) Try A direct Match
		if (logger.isDebugEnabled()) {
			logger.debug("Applying Direct Match on UA: " + userAgent);
		}
		String deviceID = filteredDevices.getDevice(userAgent);
		if (!isNullOrGeneric(deviceID)) {
			return deviceID;
		}

		// 2) Try A Conclusive Match
		if (logger.isDebugEnabled()) {
			logger.debug("Applying Conclusive Match on UA: " + userAgent);
		}
		deviceID = applyConclusiveMatch(request, filteredDevices);

		if (!isNullOrGeneric(deviceID)) {
			return deviceID;
		}

		// 3) Try A Recovery Match
		if (logger.isDebugEnabled()) {
			logger.debug("Applying Recovery Match on UA: " + userAgent);
		}
		deviceID = applyRecoveryMatch(request, filteredDevices);

		if (!isNullOrGeneric(deviceID)) {
			return deviceID;
		}

		// 4) CatchAll Match
		if (logger.isDebugEnabled()) {
			logger
					.debug("Applying CatchAll Recovery Match on UA: "
							+ userAgent);
		}
		deviceID = applyCatchAllRecoveryMatch(request, filteredDevices);

		assert deviceID != null;

		return deviceID;
	}

	/**
	 * Apply conclusive matching strategy, at least returns <code>generic</code>.
	 * 
	 * @param request
	 *            The request used to applying matching strategy.
	 * @return The matched device identifiers.
	 */
	protected String applyConclusiveMatch(WURFLRequest request,
			FilteredDevices filteredDevices) {

		if (logger.isTraceEnabled()) {
			logger.trace("Matching UA: " + request.getUserAgent()
					+ " against devices: " + filteredDevices.getUserAgents());
		}

		String match = lookForMatchingUserAgent(
				filteredDevices.getUserAgents(), request.getUserAgent());

		// Log undetected
		if (undetected.isDebugEnabled() && match == null) {
			undetected.debug(request.getUserAgent() + ":"
					+ request.getUserAgentProfile());
		}

		String deviceID = Constants.GENERIC;

		if (null != match) {

			if (detected.isDebugEnabled()) {
				detected.debug(request.getUserAgent() + ":"
						+ request.getUserAgentProfile() + ":" + match);
			}

			deviceID = filteredDevices.getDevice(match);
		}

		// deviceID maybe null if filteredDevices contains null values
		if (deviceID == null) {

			if (logger.isWarnEnabled()) {
				logger.warn("filteredDevices do not contain UA: " + match
						+ " return generic");
			}

			deviceID = Constants.GENERIC;
		}

		return deviceID;

	}

	/**
	 * Match the given user-agent to collected user-agents.
	 * 
	 * <p>
	 * Apply RIS(FS) to default.
	 * </p>
	 * 
	 * @param userAgentsSet
	 *            A SortedSet of String containing user-agent string naturally
	 *            ordered.
	 * @param userAgent
	 *            The user-agent to match against.
	 * 
	 * @return The collected user-agent matching with given user-agent.
	 */
	protected String lookForMatchingUserAgent(SortedSet userAgentsSet,
			String userAgent) {

		int tollerance = StringUtils.firstSlash(userAgent);

		if (logger.isDebugEnabled()) {
			logger.debug("Applying RIS(FS) UA: " + userAgent);
		}

		return StringUtils.risMatch(userAgentsSet, userAgent, tollerance);

	}

	/**
	 * Match the given request using the recovery matching strategy.
	 * 
	 * @param request
	 *            The request to match.
	 * @return The device identifier matched from request, at least returns
	 *         <code>generic</code>.
	 */
	protected String applyRecoveryMatch(WURFLRequest request,
			FilteredDevices filteredDevices) {
		return Constants.GENERIC;
	}

	/**
	 * <p>
	 * Match the given request using the CatchAll strategy. The subclass can
	 * override this method, but must returns "generic" device al least.
	 * </p>
	 * <p>
	 * <b>
	 * TODO this method presume the wurfl.xml + patch files must containig the
	 * hard coded device's ids.
	 * </b>
	 * </p>
	 * @param request
	 *            The request to match.
	 * @return the matched device's id, at least "generic".
	 */
	protected String applyCatchAllRecoveryMatch(WURFLRequest request,
			FilteredDevices filteredDevices) {

		String userAgent = request.getUserAgent();

		// Openwave
		if (userAgent.indexOf("UP.Browser/7.2") != -1) {
			return "opwv_v72_generic";
		}
		if (userAgent.indexOf("UP.Browser/7") != -1) {
			return "opwv_v7_generic";
		}
		if (userAgent.indexOf("UP.Browser/6.2") != -1) {
			return "opwv_v62_generic";
		}
		if (userAgent.indexOf("UP.Browser/6") != -1) {
			return "opwv_v6_generic";
		}
		if (userAgent.indexOf("UP.Browser/5") != -1) {
			return "upgui_generic";
		}
		if (userAgent.indexOf("UP.Browser/4") != -1) {
			return "uptext_generic";
		}
		if (userAgent.indexOf("UP.Browser/3") != -1) {
			return "uptext_generic";
		}

		// Series 60
		if (userAgent.indexOf("Series60") != -1) {
			return "nokia_generic_series60";
		}

		// Access/Net Front
		if (userAgent.indexOf("NetFront/3.0") != -1
				|| userAgent.indexOf("ACS-NF/3.0") != -1) {
			return "netfront_ver3";
		}
		if (userAgent.indexOf("NetFront/3.1") != -1
				|| userAgent.indexOf("ACS-NF/3.1") != -1) {
			return "netfront_ver3_1";
		}
		if (userAgent.indexOf("NetFront/3.2") != -1
				|| userAgent.indexOf("ACS-NF/3.2") != -1) {
			return "netfront_ver3_2";
		}
		if (userAgent.indexOf("NetFront/3.3") != -1
				|| userAgent.indexOf("ACS-NF/3.3") != -1) {
			return "netfront_ver3_3";
		}
		if (userAgent.indexOf("NetFront/3.4") != -1) {
			return "netfront_ver3_4";
		}
		if (userAgent.indexOf("NetFront/3.5") != -1) {
			return "netfront_ver3_5";
		}

		// Windows CE

		if (userAgent.indexOf("Windows CE") != -1) {
			return "ms_mobile_browser_ver1";
		}
		
		// web browsers?
		if (userAgent.indexOf("Mozilla/4.0") != -1) {
			return "generic_web_browser";
		}
		if (userAgent.indexOf("Mozilla/5.0") != -1) {
			return "generic_web_browser";
		}
		if (userAgent.indexOf("Mozilla/6.0") != -1) {
			return "generic_web_browser";
		}

		// Generic XHTML
		if (userAgent.indexOf("Mozilla/") != -1) {
			return Constants.GENERIC_XHTML;
		}

		if ((userAgent.indexOf("ObigoInternetBrowser/Q03C") != -1)
				|| userAgent.indexOf("AU-MIC/2") != -1
				|| userAgent.indexOf("AU-MIC-") != -1
				|| userAgent.indexOf("AU-OBIGO/") != -1
				|| userAgent.indexOf("Obigo/Q03") != -1
				|| userAgent.indexOf("Obigo/Q04") != -1
				|| userAgent.indexOf("ObigoInternetBrowser/2") != -1
				|| userAgent.indexOf("Teleca Q03B1") != -1) {
			return Constants.GENERIC_XHTML;
		}


		// Opera Mini
		if (userAgent.indexOf("Opera Mini/1") != -1) {
			return "opera_mini_ver1";
		}
		if (userAgent.indexOf("Opera Mini/2") != -1) {
			return "opera_mini_ver2";
		}
		if (userAgent.indexOf("Opera Mini/3") != -1) {
			return "opera_mini_ver3";
		}
		if (userAgent.indexOf("Opera Mini/4") != -1) {
			return "opera_mini_ver4";
		}

		// DoCoMo
		if (userAgent.startsWith("DoCoMo") || userAgent.startsWith("KDDI")) {
			return "docomo_generic_jap_ver1";
		}
		
		// returns generic as last chance.
		return Constants.GENERIC;
	}

	/**
	 * Return if the given device identifier is null or <code>generic</code>.
	 * 
	 * @param deviceID
	 *            The device identifier to check.
	 * @return True if <code>deviceID</code> is null or <code>generic</code>.
	 */
	private boolean isNullOrGeneric(String deviceID) {
		return deviceID == null || Constants.GENERIC.equals(deviceID);
	}

}
